/*
 * Created on Jun 17, 2005
 *
 * TODO To change the template for this generated file go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
/**
 * @author David Armstrong
 *
 * TODO To change the template for this generated type comment go to
 * Window - Preferences - Java - Code Style - Code Templates
 */
package custom.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.Closeable;
import java.nio.channels.FileChannel;
import java.nio.MappedByteBuffer;
import java.util.List;

public class FileCopy {

    private FileCopyHelper fileCopyHelper = null;
    private boolean reportCopyStatus = false;
    private FilenameFilter filenameCopyFilter = null;
    private FilenameFilter filenameFlattenFilter = null;
    private FilenameFilter filenameMergeFilter = null;
    private int maxDirCopyLevel = COPY_ALL_DIRS;
    private int maxDirFlattenLevel = NO_FLATTEN;
    private int maxDirMergeLevel = NO_MERGE;
    
    // Return values for ensureValidSource, ensureValidDestinationParent
    private static final int VALID_FILE = 0;
    private static final int NO_FILE = 1; // File does not exist
    private static final int NOT_DIR = 2; // Should be a directory, but it is a file
    private static final int NO_WRITE = 3; // Directory not writeable
    private static final int NO_READ = 4; // File not readable
    
    // Constant for value that means do not flatten directories
    public static final int NO_FLATTEN = -1;
    // Constant for value that means do not merge directories
    public static final int NO_MERGE = -1;
    public static final int FLATTEN_ALL_DIRS = 0;
    public static final int MERGE_ALL_DIRS = 0;
    public static final int COPY_ALL_DIRS = 0;

    public FileCopy(final FileCopyHelper fileCopyHelper) {
        setFileCopyHelper(fileCopyHelper);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, final FilenameFilter filenameCopyFilter,
            final FilenameFilter filenameFlattenFilter, final FilenameFilter filenameMergeFilter) {
        setFileCopyHelper(fileCopyHelper);
        setFilenameCopyFilter(filenameCopyFilter);
        setFilenameFlattenFilter(filenameFlattenFilter);
        setFilenameMergeFilter(filenameMergeFilter);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, boolean reportCopyStatus) {
        setFileCopyHelper(fileCopyHelper);
        setReportCopyStatus(reportCopyStatus);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, boolean reportCopyStatus, int maxDirFlattenLevel) {
        this(fileCopyHelper, reportCopyStatus);
        setMaxDirFlattenLevel(maxDirFlattenLevel);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, boolean reportCopyStatus, int maxDirFlattenLevel,
            int maxDirMergeLevel) {
        this(fileCopyHelper, reportCopyStatus, maxDirFlattenLevel);
        setMaxDirMergeLevel(maxDirMergeLevel);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, boolean reportCopyStatus, int maxDirFlattenLevel,
            int maxDirMergeLevel, int maxDirCopyLevel) {
        this(fileCopyHelper, reportCopyStatus, maxDirFlattenLevel, maxDirMergeLevel);
        setMaxDirCopyLevel(maxDirCopyLevel);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, final FilenameFilter filenameCopyFilter,
            final FilenameFilter filenameFlattenFilter, final FilenameFilter filenameMergeFilter,
            boolean reportCopyStatus, int maxDirFlattenLevel) {
        this(fileCopyHelper, reportCopyStatus, maxDirFlattenLevel);
        setFilenameCopyFilter(filenameCopyFilter);
        setFilenameFlattenFilter(filenameFlattenFilter);
        setFilenameMergeFilter(filenameMergeFilter);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, final FilenameFilter filenameCopyFilter,
            final FilenameFilter filenameFlattenFilter, final FilenameFilter filenameMergeFilter,
            boolean reportCopyStatus, int maxDirFlattenLevel, int maxDirMergeLevel) {
        this(fileCopyHelper, filenameCopyFilter, filenameFlattenFilter, filenameMergeFilter, reportCopyStatus,
                maxDirFlattenLevel);
        setMaxDirMergeLevel(maxDirMergeLevel);
    }

    public FileCopy(final FileCopyHelper fileCopyHelper, final FilenameFilter filenameCopyFilter,
            final FilenameFilter filenameFlattenFilter, final FilenameFilter filenameMergeFilter,
            boolean reportCopyStatus, int maxDirFlattenLevel, int maxDirMergeLevel, int maxDirCopyLevel) {
        set(fileCopyHelper, filenameCopyFilter, filenameFlattenFilter, filenameMergeFilter, reportCopyStatus,
                maxDirFlattenLevel, maxDirMergeLevel, maxDirCopyLevel);
    }

    public void setFileCopyHelper(final FileCopyHelper fileCopyHelper) {
        if (fileCopyHelper == null) {
            throw new IllegalArgumentException("FileCopy must have a FileCopyHelper object.");
        }
        this.fileCopyHelper = fileCopyHelper;
    }

    public void setReportCopyStatus(boolean reportCopyStatus) {
        this.reportCopyStatus = reportCopyStatus;
    }

    public void setMaxDirFlattenLevel(int maxFlattenLevel) {
        this.maxDirFlattenLevel = maxFlattenLevel;
    }

    public void setMaxDirMergeLevel(int maxMergeLevel) {
        this.maxDirMergeLevel = maxMergeLevel;
    }

    public void setFilenameCopyFilter(final FilenameFilter filenameCopyFilter) {
        this.filenameCopyFilter = filenameCopyFilter;
    }

    public void setFilenameFlattenFilter(final FilenameFilter filenameFlattenFilter) {
        this.filenameFlattenFilter = filenameFlattenFilter;
    }

    public void setFilenameMergeFilter(final FilenameFilter filenameMergeFilter) {
        this.filenameMergeFilter = filenameMergeFilter;
    }

    public void setMaxDirCopyLevel(int maxDirLevel) {
        this.maxDirCopyLevel = maxDirLevel;
    }

    public void set(final FileCopyHelper fileCopyHelper, final FilenameFilter filenameCopyFilter,
            final FilenameFilter filenameFlattenFilter, final FilenameFilter filenameMergeFilter,
            boolean reportCopyStatus, int maxDirFlattenLevel, int maxDirMergeLevel, int maxDirCopyLevel) {
        setFileCopyHelper(fileCopyHelper);
        setFilenameCopyFilter(filenameCopyFilter);
        setFilenameFlattenFilter(filenameFlattenFilter);
        setFilenameMergeFilter(filenameMergeFilter);
        setReportCopyStatus(reportCopyStatus);
        setMaxDirFlattenLevel(maxDirFlattenLevel);
        setMaxDirMergeLevel(maxDirMergeLevel);
        setMaxDirCopyLevel(maxDirCopyLevel);
    }

    /**
     * This copy method copies one file that is not a directory to one destination.
     *
     */
    public static void copy(final File source, File dest, boolean doOverwrite) throws IOException, FileNotFoundException {
        if (dest == null) {
            throw new IllegalArgumentException("Destination file object is null.");
        }
        if (source == null) {
            throw new IllegalArgumentException("Source file object is null.");
        }

        // Make sure the copy file exists and is readable.
        int status = ensureValidSource(source);
        if (status != VALID_FILE) {
            if (status == NO_FILE) {
                // If destination is a directory, create a new file instance for the directory + the source file name
                if (dest.isDirectory()) {
                    dest = new File(dest, source.getName());
                }
                throw new FileNotFoundException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + source + "\" does not exist.");
            } else if (status == NO_READ) {
                // If destination is a directory, create a new file instance for the directory + the source file name
                if (dest.isDirectory()) {
                    dest = new File(dest, source.getName());
                }
                throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + source + "\" is not readable.");
            }
        }

        // This method cannot copy directories.
        if (!source.isFile()) {
            throw new FileNotFoundException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + source + "\" is a directory.");
        }

        // If destination is a directory, create a new file instance for the directory + the source file name
        if (dest.isDirectory()) {
            dest = new File(dest, source.getName());
        }

        // If file exists, determine to go on or not based on if bOverwrite is set to true.
        if (dest.exists()) {
            // If doOverwrite is true, make sure destination file is writeable
            if (doOverwrite) {
                if (!dest.canWrite()) {
                    throw new FileNotFoundException("FileCopy: " + "destination file is unwriteable: " + dest);
                }
            } // else bail out.
            else {
                throw new IOException("FileCopy: " + "destination file exists: " + dest);
            }
        } // Destination file does not exist. Make sure its directory exists and that it is writeable
        else {
            status = ensureValidDestinationParent(dest);
            if (status != VALID_FILE) {
                if (status == NO_FILE) {
                    throw new FileNotFoundException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + (dest.getParent() != null ? dest.getParent() : dest) + "\" does not exist.");
                } else if (status == NOT_DIR) {
                    throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + dest.getParent() + "\" is not a directory.");
                } else if (status == NO_WRITE) {
                    throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + dest.getParent() + "\" is not writeable.");
                }
            }
        }

        // Copy the file
        copyFile(source, dest);
    }

    /**
     * This copy method can be used to copy a source directory to a destination directory.
     *
     */
    public int copy(final File source, File dest) throws FileCopyCancelException {
        return copy(source, dest, 0);
    }

    private int copy(final File source, File dest, int curDirLevel) throws FileCopyCancelException {
        if (dest == null) {
            throw new IllegalArgumentException("Destination file object is null.");
        }
        if (source == null) {
            throw new IllegalArgumentException("Source file object is null.");
        }

        boolean status = true;
        int copyCt = 0;

        String copyStatusSuccessPreface = "File copied from ";
        String copyStatusFailurePreface = "File not copied from ";
        try {
            int valid = ensureValidSource(source);
            if (valid != VALID_FILE) {
                if (valid == NO_FILE) {
                    // If destination is a directory, create a new file instance for the directory + the source file name
                    if (dest.isDirectory()) {
                        dest = new File(dest, source.getName());
                    }
                    throw new FileNotFoundException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + source + "\" does not exist.");
                } else if (valid == NO_READ) {
                    // If destination is a directory, create a new file instance for the directory + the source file name
                    if (dest.isDirectory()) {
                        dest = new File(dest, source.getName());
                    }
                    throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + source + "\" is not readable.");
                }
            }

            if (source.isDirectory()) {
                // If maxDirCopyLevel is less than 1, the default is to copy all levels of directories. If it is greater
                // than 0, that is the max level of directories to copy.
                ++curDirLevel;
                //if((maxDirCopyLevel < 1) || (curDirLevel <= maxDirCopyLevel))
                if (toCopy(source, curDirLevel)) {
                    copyCt += copyDirectory(source, dest, curDirLevel);
                }
            } else {
                // If destination is a directory, create a new file instance for the directory + the source file name
                String name = source.getName();
                if (dest.isDirectory()) {
                    dest = new File(dest, name);
                }

                //boolean copyFile = true;
                if (dest.exists()) {
                    status = fileCopyHelper.handleExistingFile(dest, source);
                }

                if (status) {
                    valid = ensureValidDestinationParent(dest);
                    if (valid != VALID_FILE) {
                        if (valid == NO_FILE) {
                            throw new FileNotFoundException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + (dest.getParent() != null ? dest.getParent() : dest) + "\" does not exist.");
                        } else if (valid == NOT_DIR) {
                            throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + dest.getParent() + "\" is not a directory.");
                        } else if (valid == NO_WRITE) {
                            throw new IOException("File not copied from \"" + source + "\" to \"" + dest + "\" because \"" + dest.getParent() + "\" is not writeable.");
                        }
                    }

                    copyFile(source, dest);
                    copyCt++;
                }

                //if(!maxDirLevelReached && reportCopyStatus)
                if (reportCopyStatus) {
                    String copyStatusPreface = status ? copyStatusSuccessPreface : copyStatusFailurePreface;
                    fileCopyHelper.reportCopyStatus(copyStatusPreface + "\"" + source + "\" to \"" + dest + "\"", status);
                }
            }

        } catch (IOException e) {
            status = handleCopyFailure(dest, source, e);
        }

        return copyCt;
    }

    /**
     * This copy method can be used to copy a source directory to a destination directory.
     *
     */
    public int copy(final File source, File dest, final FileCopyHelper fileCopyHelper,
            final FilenameFilter filenameCopyFilter, final FilenameFilter filenameFlattenFilter,
            final FilenameFilter filenameMergeFilter, boolean reportCopyStatus,
            int maxFlattenLevel, int maxDirLevel, int maxMergeLevel) throws FileCopyCancelException {
        set(fileCopyHelper, filenameCopyFilter, filenameFlattenFilter, filenameMergeFilter, reportCopyStatus,
                maxFlattenLevel, maxDirLevel, maxMergeLevel);
        return (copy(source, dest, 0));
    }

    /**
     * The next four methods are wrappers for copying multiple files to multiple locations.
     */
    public int copy(final File source, final List<File> dests) throws FileCopyCancelException {
        if (dests == null) {
            throw new IllegalArgumentException("List of destination file objects is null.");
        }

        int copyCt = 0;
        for (File dest : dests) {
            copyCt += copy(source, dest, 0);
        }
        return copyCt;
    }

    public int copy(final List<File> sources, File dest) throws FileCopyCancelException {
        if (sources == null) {
            throw new IllegalArgumentException("List of source file objects is null.");
        }

        int copyCt = 0;
        for (File source : sources) {
            copyCt += copy(source, dest, 0);
        }
        return copyCt;
    }

    public int copy(final List<File> sources, final List<File> dests) throws FileCopyCancelException {
        if (dests == null) {
            throw new IllegalArgumentException("List of destination file objects is null.");
        }
        if (sources == null) {
            throw new IllegalArgumentException("List of source file objects is null.");
        }

        int copyCt = 0;
        for (File dest : dests) {
            for (File source : sources) {
                copyCt += copy(source, dest, 0);
            }
        }

        return copyCt;
    }

    public int copy(final File source, final File[] dests) throws FileCopyCancelException {
        if (dests == null) {
            throw new IllegalArgumentException("Array of destination file objects is null.");
        }

        int copyCt = 0;
        for (File dest : dests) {
            copyCt += copy(source, dest, 0);
        }
        return copyCt;
    }

    public int copy(final File[] sources, File dest) throws FileCopyCancelException {
        if (sources == null) {
            throw new IllegalArgumentException("Array of source file objects is null.");
        }

        int copyCt = 0;
        for (File source : sources) {
            copyCt += copy(source, dest, 0);
        }
        return copyCt;
    }

    public int copy(final File[] sources, final File[] dests) throws FileCopyCancelException {
        if (dests == null) {
            throw new IllegalArgumentException("Array of destination file objects is null.");
        }
        if (sources == null) {
            throw new IllegalArgumentException("Array of source file objects is null.");
        }

        int copyCt = 0;
        for (File dest : dests) {
            for (File source : sources) {
                copyCt += copy(source, dest, 0);
            }
        }

        return copyCt;
    }

    private static void copyFile(final File source, final File dest) throws FileNotFoundException, IOException {

        FileInputStream inputSource = null;
        FileOutputStream inputDest = null;
        FileChannel chanSource = null;
        FileChannel chanDest = null;

        try {
            // Copy the file
            inputSource = new FileInputStream(source);
            inputDest = new FileOutputStream(dest);

            // Get channels to the input and output files.
            chanSource = inputSource.getChannel();
            chanDest = inputDest.getChannel();

            // Get the size of the source file
            long size = chanSource.size();

            // Map the input file to a buffer
            MappedByteBuffer mappedBuffer = chanSource.map(FileChannel.MapMode.READ_ONLY, 0, size);

            // Write the buffer to the output file
            chanDest.write(mappedBuffer);

            // Set the destination file's last modified date to the same as the source file's last modified date.
            dest.setLastModified(source.lastModified());
        } finally {
            // Close the channels and files
            closeIgnoringException(chanSource);
            closeIgnoringException(chanDest);
            closeIgnoringException(inputSource);
            closeIgnoringException(inputDest);
        }

    }

    private static void closeIgnoringException(Closeable c) {
        if(c != null) {
            try { c.close(); } catch(IOException e) {} //ignore
        }
    }

    /**
     * This function copies a file that is a folder.
     */
    private int copyDirectory(final File source, final File dest, int curDirLevel) throws IOException, FileCopyCancelException {
        // The source file is a folder. The destination file must be, also.
        if (!dest.isDirectory()) {
            throw new IOException("FileCopy: source file \"" + source + "\" is a directory. and destination file \"" + dest + "\" is not.");
        }

        File newDestFolder;
        boolean copyFile = true;
        int copyCt = 0;

        // See if we have a filter to flatten certain directories.
        //boolean flattenDir = filenameFlattenFilter != null ?
        //filenameFlattenFilter.accept(source.getParentFile(), source.getName()) : false;

        boolean flattenDir = toFlatten(source, curDirLevel);

        // If we are not supposed to flatten this particular directory (flattenDir) and maxDirFlattenLevel is not
        // FLATTEN_ALL_DIRS and it is less than the current directory level, make a copy of the directory
        //if(!flattenDir && (maxDirFlattenLevel != FLATTEN_ALL_DIRS && maxDirFlattenLevel < curDirLevel))
        if (!flattenDir) {
            // Create a new folder in the destination folder with the name of the source folder.
            newDestFolder = new File(dest.getPath() + System.getProperty("file.separator") + source.getName());

            // See if we have a filter to merge certain directories.
            //boolean mergeDir = filenameMergeFilter != null ?
            //filenameMergeFilter.accept(source.getParentFile(), source.getName()) : false;
            boolean mergeDir = toMerge(source, curDirLevel);

            if (newDestFolder.exists()) {
                // If we are not supposed to merge this particular directory (mergeDir) and maxDirMergeLevel is not
                // MERGE_ALL_DIRS or it is less than the current directory level, do not merge the directories.
                //if(!mergeDir && (maxDirMergeLevel != MERGE_ALL_DIRS && maxDirMergeLevel < curDirLevel))
                if (!mergeDir) {
                    // This method should rename the directory to something else.
                    copyFile = fileCopyHelper.handleExistingFile(newDestFolder, source);
                }
            } // If the folder did not exist and we are supposed to merge this particular directory (mergeDir), or
            // the maxDirMergeLevel is set to MERGE_ALL_DIRS or is greater than or equal to the curDirLevel, we need
            // to create the directory. This is the only case in which we will create a directory if we are
            // supposed to merge.
            else if (mergeDir) {
                // We need to create the destination folder.
                newDestFolder.mkdir();
            }

            // Only make a directory if copyFile is true AND we are not supposed to merge.
            if (copyFile && (maxDirMergeLevel != MERGE_ALL_DIRS && maxDirMergeLevel < curDirLevel)) {
                // We need to create the destination folder. It either did not exist, or did, but we renamed it. We
                // only would have renamed it if we are not supposed to merge.
                newDestFolder.mkdir();
            }
        } else {
            // We are not to create the root folder, so set the newDestFolder to the dest.
            newDestFolder = dest;
        }

        if (copyFile) {
            File[] srcFiles = filenameCopyFilter != null ? source.listFiles(filenameCopyFilter) : source.listFiles();

            for (File file : srcFiles) {
                /* If copy fails, set copyFile to false. This way if it fails even once copyFile will be set to false, which is
                 * what we want. */
                copyCt += copy(file, newDestFolder, curDirLevel);
            }
        }

        return copyCt;
    }

    private boolean toCopy(final File source, int curDirLevel) {
        // Only check for a filenameCopyFilter if the current directory level is 1. If it is greater than 1,
        // this directory has already passed this check.
        boolean copy = (curDirLevel == 1 && filenameCopyFilter != null) ? filenameCopyFilter.accept(source.getParentFile(), source.getName()) : true;

        if (copy && source.isDirectory()) {
            // See if we are supposed to copy all directories at this level.
            copy = (maxDirCopyLevel == COPY_ALL_DIRS || maxDirCopyLevel >= curDirLevel);
        }

        return copy;
    }

    private boolean toFlatten(final File source, int curDirLevel) {
        // See if we have a filter to flatten certain directories.
        boolean flatten = filenameFlattenFilter != null ? filenameFlattenFilter.accept(source.getParentFile(), source.getName()) : false;

        if (!flatten) {
            // See if we are supposed to flatten all directories at this level. If maxDirFlattenLevel
            // equals FLATTEN_ALL_DIRS, we are supposed to flatten all levels.
            flatten = (maxDirFlattenLevel == FLATTEN_ALL_DIRS || maxDirFlattenLevel >= curDirLevel);
        }

        return flatten;
    }

    private boolean toMerge(final File source, int curDirLevel) {
        // See if we have a filter to merge certain directories.
        boolean merge = filenameMergeFilter != null ? filenameMergeFilter.accept(source.getParentFile(), source.getName()) : false;

        if (!merge) {
            // See if we are supposed to merge all directories at this level. If maxDirMergeLevel
            // equals MERGE_ALL_DIRS, we are supposed to merge all levels.
            merge = (maxDirMergeLevel == MERGE_ALL_DIRS || maxDirMergeLevel >= curDirLevel);
        }

        return merge;
    }

    private static int ensureValidDestinationParent(final File dest) {
        int status = VALID_FILE;
        String parent = dest.getParent();
        // Get current directory if parent is not specified in the File instance
        if (parent == null) {
            parent = System.getProperty("user.dir");
            if (!dest.exists()) {
                status = NO_FILE;
            }
        } else {
            File dir = new File(parent);
            if (!dir.exists()) {
                status = NO_FILE;
            } else if (dir.isFile()) {
                status = NOT_DIR;
            } else if (!dir.canWrite()) {
                status = NO_WRITE;
            }
        }

        return status;
    }

    private static int ensureValidSource(final File source) {
        int status = VALID_FILE;
        // Make sure we the copy file exists, is a file and is readable
        if (!source.exists()) {
            status = NO_FILE;
        } else if (!source.canRead()) {
            status = NO_READ;
        }

        return status;
    }

    private boolean handleCopyFailure(final File dest, final File source, final IOException e) {
        boolean status = false;
        // If the file could not be overwritten because it is in use, call fileCopyHelper's handleFileInUse function to 
        // rename the file. If this succeeds, try to copy again.
        if (e.getMessage().contains("The process cannot access the file because it is being used by another process")) {
            if (fileCopyHelper.handleFileInUse(dest)) {
                try {
                    copyFile(dest, source);
                    status = true;
                } catch (IOException ie) {
                    fileCopyHelper.handleException(ie);
                }
            }
        } else {
            fileCopyHelper.handleException(e);
        }

        return status;
    }
}
